#pragma once

#include "ethernet_context.hpp"
#include "limbo/customization.hpp"
#include "limbo/details/crc32.h"
#include "limbo/details/traits.hpp"
#include "limbo/errc.h"
#include "limbo/order.hpp"
#include "limbo/result.h"
#include "opts.h"
#include <algorithm>
#include <cstdint>

namespace limbo::ethernet {

namespace impl {

template <typename State> static auto constexpr calc_trailer() -> uint8_t {
  constexpr auto has_crc =
      (State::opts & Opts::calculate_crc) | (State::opts & Opts::validate_crc);
  if constexpr (has_crc) {
    return 4;
  } else {
    return 0;
  }
}

} // namespace impl

template <typename State> struct EthernetFrame {
  using LowerState = typename State::LowerState;
  using Context = EthernetContext<State>;
  using Customization = customization<details::base_state_t<State>>;

  static constexpr uint8_t HEADER_SZ = 14;
  static constexpr uint8_t BODY_SZ = 46;
  static constexpr uint8_t TRAILER_SZ = impl::calc_trailer<State>();
  static constexpr uint8_t MIN_SZ = HEADER_SZ + BODY_SZ + TRAILER_SZ;

  MacAddress source;
  MacAddress destination;
  uint16_t type;
  Chunk payload;
  uint32_t crc;

  inline static uint32_t prefix(const Context &) { return HEADER_SZ; }

  static Result send(State &state, const Context &ctx, Chunk buff,
                     const Chunk payload) {
    if (ctx.destination == MacAddress::unknown) {
      auto code = (uint32_t)Errc::ethernet_unknown_mac;
      return Result::make_error(code, &state);
    }
    bool no_enough_space =
        (buff.size() < MIN_SZ) ||
        (buff.size() < payload.size() + HEADER_SZ + TRAILER_SZ);
    if (no_enough_space) {
      auto code = (uint32_t)Errc::no_enough_space;
      return Result::make_error(code, &state);
    }

    auto ptr = buff.data();

    auto dst = ctx.destination.bytes;
    Customization::copy(ptr, dst, MacAddress::SZ);
    ptr += MacAddress::SZ;

    auto src = ctx.source.bytes;
    Customization::copy(ptr, src, MacAddress::SZ);
    ptr += MacAddress::SZ;

    auto type = order::native_to_big((uint16_t)ctx.type);
    Customization::copy(ptr, &type, sizeof(type));
    ptr += sizeof(type);

    auto consumed = Chunk(buff.data(), HEADER_SZ);
    Customization::copy(consumed, payload);
    auto under_fill =
        (int)MIN_SZ - (int)(payload.size() + HEADER_SZ + TRAILER_SZ);
    if (under_fill > 0) {
      auto ptr = consumed.data() + consumed.size();
      auto end = ptr + under_fill;
      while (ptr != end) {
        *ptr++ = 0;
      }
      consumed = Chunk(buff.data(), MIN_SZ - TRAILER_SZ);
    }

    if constexpr (State::opts & Opts::calculate_crc) {
      uint32_t crc = 0;
      crc = details::crc32(consumed.data(), consumed.size());
      Customization::copy(consumed, Chunk(&crc, sizeof(crc)));
    }

    return Result::make_consumed(&state, consumed);
  }

  Result recv(Chunk buff, State &state, LowerState *) {
    auto sz = buff.size();
    if (sz < MIN_SZ) {
      auto code = (uint32_t)Errc::ethernet_malformed_frame;
      return Result::make_error(code, &state, buff);
    }
    auto ptr = (unsigned char *)buff.data();

    destination = MacAddress((unsigned char *)ptr);
    ptr += MacAddress::SZ;

    source = MacAddress((unsigned char *)ptr);
    ptr += MacAddress::SZ;

    uint16_t t;
    Customization::copy(&t, ptr, sizeof(t));
    type = order::big_to_native(t);
    ptr += sizeof(t);

    payload = Chunk(ptr, buff.size() - (HEADER_SZ + TRAILER_SZ));

    if constexpr (State::opts & Opts::validate_crc) {
      ptr = payload.data() + payload.size();
      Customization::copy(&crc, ptr, sizeof(crc));
      auto value = details::crc32(buff.data(), buff.size() - TRAILER_SZ);
      if (value != crc) {
        auto code = (uint32_t)Errc::ethernet_crc_mismatch;
        return Result::make_error(code, &state, buff);
      }
    } else {
      crc = 0;
    }

    /* success */
    return Result::make_consumed(&state, buff);
  }
};

template <typename State>
inline static uint32_t is_ip_packet(const EthernetFrame<State> &frame) {
  return frame.type == (uint16_t)EtherType::ipv4;
}

template <typename State>
inline static uint32_t is_arp_packet(const EthernetFrame<State> &frame) {
  return frame.type == (uint16_t)EtherType::arp;
}

template <typename State>
inline static uint32_t get_prefix(const EthernetContext<State> &) {
  return EthernetFrame<State>::HEADER_SZ;
}

} // namespace limbo::ethernet
